﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Threading;
using Citrix.Management.Automation;
using Citrix.XaAdminMobile.Util;
using Citrix.XenApp.Commands;
using Citrix.XenApp.Sdk;
using System.Management.Automation;
using System.ComponentModel;
using Citrix.Cmp;
using System.Drawing;

namespace Citrix.XaAdminMobile.Model
{
    /// <summary>
    /// Container and manager for all session model objects.
    /// </summary>
    internal class SessionsModel : ISessionsModel
    {
        /// <summary>
        /// Syncs access to internal state.
        /// </summary>
        private object stateLock = new object();

        /// <summary>
        /// Ref back to the base/parent model object.
        /// </summary>
        private readonly DefaultModel modelBase;

        /// <summary>
        /// A ref count of session load actions. This tracks both the bulk loads of multiple
        /// sessions we do in this class as well as any individual sessions that are reloading
        /// their state.
        /// </summary>
        private int loadingRefCount = 0;

        /// <summary>
        /// The locally cached set of known sessions.
        /// </summary>
        private Dictionary<SessionKey, SessionModel> sessions = new Dictionary<SessionKey, SessionModel>();

        /// <summary>
        /// The last error returned when loading server state - null means no error.
        /// </summary>
        private string lastLoadError = null;

        /// <summary>
        /// Fired whenever a new session(s) has been detected, or when an existing session(s)
        /// has been removed.
        /// </summary>
        public event EventHandler<SessionsChangedArgs> SessionsChanged;

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="modelRoot">The root model object.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="modelRoot"/> is
        /// null.</exception>
        public SessionsModel(DefaultModel modelRoot)
        {
            if (null == modelRoot)
            {
                throw new ArgumentNullException("modelRoot");
            }

            this.modelBase = modelRoot;
            LoadSessions();
        }

        /// <summary>
        /// Whether or not there are any active queries to load session state.
        /// </summary>
        public bool IsLoading
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.loadingRefCount > 0;
                }
            }
        }

        /// <summary>
        /// Returns the last error (if any) when loading server state. Null means the last load
        /// was successful.
        /// </summary>
        public string LastLoadError
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.lastLoadError;
                }
            }
            private set
            {
                bool changed = false;
                lock (this.stateLock)
                {
                    if (!string.Equals(value, this.lastLoadError))
                    {
                        changed = true;
                        this.lastLoadError = value;
                    }
                }

                if (changed)
                {
                    OnPropertyChanged("LastLoadError");
                }
            }
        }

        /// <summary>
        /// Used to track whether we have any threads loading state for sessions. We increment
        /// the load count using this function when doing a bulk load in this class, or when
        /// loading individual sessions. Whenever we have a non-zero load count we will display
        /// a loading status on the main session list page (SessionsView).
        /// </summary>
        internal void IncrementSessionLoadCount()
        {
            bool changed = false;
            lock (this.stateLock)
            {
                ++this.loadingRefCount;
                changed = (this.loadingRefCount == 1);
            }

            if (changed)
            {
                OnPropertyChanged("IsLoading");
            }
        }

        /// <summary>
        /// Decrements the session load count, <see cref="IncrementSessionLoadCount"/>.
        /// </summary>
        internal void DecrementSessionLoadCount()
        {
            bool changed = false;
            lock (this.stateLock)
            {
                if (this.loadingRefCount > 0)
                {
                    --this.loadingRefCount;
                    changed = this.loadingRefCount == 0;
                }
            }

            if (changed)
            {
                OnPropertyChanged("IsLoading");
            }
        }

        /// <summary>
        /// Refreshes all the session state. Old sessions are removed, new ones added and
        /// existing sessions updated.
        /// </summary>
        public void Refresh()
        {
            LoadSessions();
        }

        /// <summary>
        /// Resets the supplied set of sessions.
        /// </summary>
        /// <param name="sessions">The sessions to reset. A null/empty set is ignored.</param>
        public void Reset(List<ISessionModel> sessions)
        {
            if (null == sessions || sessions.Count == 0)
            {
                return;
            }

            ThreadPool.QueueUserWorkItem(o =>
            {
                try
                {
                    var targetSessions = (from s in sessions
                                          select ((SessionModel)s).PSSession).ToArray();

                    //
                    // Update action status so the UI knows we are trying to do something with
                    // the affected session objects.
                    //
                    foreach (var s in sessions)
                    {
                        s.SessionActionStatus = SessionActionStatus.Resetting;
                    }

                    var result = this.modelBase.RunspacePool.ExecuteCommand(
                        new StopXASessionByObject(targetSessions) { ComputerName = this.modelBase.ConnectionTarget });

                    // Remove affected sessions.
                    lock (this.stateLock)
                    {
                        foreach (SessionModel s in sessions)
                        {
                            this.sessions.Remove(s.Key);
                        }
                    }
                    // Push out session(s) remove event.
                    if (this.SessionsChanged != null && (sessions.Count > 0))
                    {
                        SessionsChangedArgs args = new SessionsChangedArgs(null, sessions);
                        this.SessionsChanged.Raise(this, args);
                    }
                }
                catch (InvalidRunspacePoolStateException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                catch (CmdletInvocationException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                catch (Exception ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    throw;
                }
                finally
                {
                    // Clear the action status.
                    foreach (var s in sessions)
                    {
                        s.SessionActionStatus = SessionActionStatus.None;
                    }
                }
            });
        }

        /// <summary>
        /// Returns the current set of session model objects. The session objects are first
        /// loaded at class instantiation. Thereafter they only get updated if the public
        /// session mutating calls on this class are called, e.g. <see cref="Refresh"/> and
        /// <see cref="Reset"/>. There is no automatic syncing/updating of sessions.
        /// </summary>
        public List<ISessionModel> Sessions
        {
            get
            {
                lock (this.stateLock)
                {
                    return new List<ISessionModel>(this.sessions.Values);
                }
            }
        }

        /// <summary>
        /// Loads/reloads the session state. The the SDK is called in background threads so the
        /// caller does not get blocked.
        /// </summary>
        private void LoadSessions()
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                try
                {
                    IncrementSessionLoadCount();
                    this.LastLoadError = null;
                    
                    // First state load, summary info only.
                    var summaryResult = this.modelBase.RunspacePool.ExecuteCommand(
                        new GetXASessionByFarm(true) { ComputerName = this.modelBase.ConnectionTarget });
                    UpdateSessions(summaryResult);
                }
                catch (InvalidRunspacePoolStateException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    this.LastLoadError = ex.Message;
                }
                catch (CmdletInvocationException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    this.LastLoadError = ex.Message;
                }
                catch (Exception ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                    throw;
                }
                finally
                {
                    DecrementSessionLoadCount();
                }
            });
        }

        /// <summary>
        /// Takes the supplied set of newly loaded session objects and updates the master set
        /// of session state. Existing objects are updated, old objects removed and new ones
        /// created (as required).
        /// </summary>
        /// <param name="sessionSource">The newly loaded set of sessions.</param>
        private void UpdateSessions(IEnumerable<XASession> sessionSource)
        {
            Dictionary<SessionKey, XASession> keyedSource = new Dictionary<SessionKey,XASession>();
            Dictionary<String, Icon> icons = new Dictionary<String, Icon>();

            if (null != sessionSource)
            {
                //
                // Convert into a keyed map. We also filter out duplicate entries. It is
                // possible for XenApp to return multiple instances back that we consider the
                // same (i.e. session ID, app name, server, logon time, etc).
                //
                foreach (var s in sessionSource)
                {
                    var key = new SessionKey(s);
                    if (!keyedSource.ContainsKey(key))
                    {
                        keyedSource[key] = s;
                    }
                }
            }

            List<ISessionModel> addedObj = new List<ISessionModel>();
            List<ISessionModel> removedObj = new List<ISessionModel>();
            List<Tuple<SessionModel, XASession>> updatedObj = new List<Tuple<SessionModel, XASession>>();

            lock (this.stateLock)
            {
                HashSet<SessionKey> addedSet = new HashSet<SessionKey>();
                HashSet<SessionKey> removedSet = new HashSet<SessionKey>(this.sessions.Keys);
                HashSet<SessionKey> updateSet = new HashSet<SessionKey>();

                // Do some basic set operations to find the 3 sets of sessions we care about.
                foreach (var kvp in keyedSource)
                {
                    if (this.sessions.ContainsKey(kvp.Key))
                    {
                        removedSet.Remove(kvp.Key);
                        updateSet.Add(kvp.Key);
                    }
                    else
                    {
                        if (kvp.Value.BrowserName != "")
                        {
                            if (!icons.ContainsKey(kvp.Value.BrowserName)) //check if we already stored the icon of this specific application
                            {
                                // get the Icon for this specific application
                                var result = this.modelBase.RunspacePool.ExecuteCommand(new GetXAApplicationIconByName(new String[] { kvp.Value.BrowserName }) { ComputerName = this.modelBase.ConnectionTarget });
                                foreach (XAApplicationIcon ctxIc in result)
                                {
                                    String base64IconString = ctxIc.EncodedIconData; //store the icon data in base64 format
                                    byte[] iconInByteArrayFormat = Util.IconConverter.Base64ToByteArray(base64IconString); //convert from base64 to regular icon bytearray format
                                    Icon ic = Util.IconConverter.ByteArrayToIcon(iconInByteArrayFormat); //turn the bytearray into Icon object
                                    icons.Add(kvp.Value.BrowserName, ic); //add it to icon dictionary
                                }
                            }
                        }
                        addedSet.Add(kvp.Key);
                    }
                }

                //
                // Convert our key sets into value sets, updating the master set of session
                // state as we go.
                //
                foreach (var s in removedSet)
                {
                    removedObj.Add(this.sessions[s]);
                    this.sessions.Remove(s);
                }
                foreach (var s in addedSet)
                {
                    var source = keyedSource[s];
                    var model = new SessionModel(source, this.modelBase, this);
                    Icon ic = null;
                    icons.TryGetValue(source.BrowserName, out ic);
                    model.ApplicationIcon = ic;
                    this.sessions[s] = model;
                    addedObj.Add(model);
                }
                foreach (var s in updateSet)
                {
                    var model = this.sessions[s];
                    var updateSource = keyedSource[s];
                    updatedObj.Add(new Tuple<SessionModel,XASession>(model, updateSource));
                }
            }

            // Push out session(s) add/remove event.
            if (this.SessionsChanged != null && (removedObj.Count > 0 || addedObj.Count > 0))
            {
                SessionsChangedArgs args = new SessionsChangedArgs(addedObj, removedObj);
                this.SessionsChanged(this, args);
            }

            // Perform updates for any existing sessions.
            foreach (var pair in updatedObj)
            {
                pair.Item1.Update(pair.Item2);
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Fired whenever a public property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fires the property change event. Maybe called from any thread - we always fire the
        /// event on the main UI thread.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        private void OnPropertyChanged(string propertyName)
        {
            NotifyHelper.VerifyProperty(propertyName, this);
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion // INotifyPropertyChanged Members
    }
}
